package simpledb.storage;

import simpledb.common.Database;
import simpledb.common.DbException;
import simpledb.common.Permissions;
import simpledb.transaction.LockManager;
import simpledb.transaction.TransactionAbortedException;
import simpledb.transaction.TransactionId;
import simpledb.util.LruCache;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

/**
 * BufferPool 是 SimpleDB 中用于管理内存中页面缓存的核心组件。它负责从磁盘读取页面到内存、将修改后的页面写回磁盘，并且支持事务并发控制中的锁机制。
 * 它是数据库系统中非常关键的一环，主要功能包括：
 * 1、页面的缓存（LRU 缓存）
 * 2、事务对页面的访问控制（加锁/解锁）
 * 3、页面的刷新与重载（提交或回滚时）
 * 4、空间不足时的页面淘汰策略
 * BufferPool 负责将磁盘上的页面读入内存进行缓存，供访问方法使用。
 * 它还负责事务对页面的锁定控制，确保事务在读写页面前获得合适的锁。
 * @Threadsafe, 所有字段都是 final 的，线程安全由 LruCache 和 LockManager 实现。
 */
public class BufferPool {

    // 每个页面的大小，默认为 4096 字节（包含页头信息） 4KB
    private static final int             DEFAULT_PAGE_SIZE = 4096;

    // 当前页面大小（可在测试时进行更改）
    private static int                   pageSize          = DEFAULT_PAGE_SIZE;

    // 默认传给构造函数的页面数量（供其他类使用），实际应使用构造函数参数 numPages
    public static final int              DEFAULT_PAGES     = 50;

    // 使用 LRU 缓存实现页面缓存
    private final LruCache<PageId, Page> lruCache;

    // 锁管理器，用于处理事务对页面的并发访问控制
    private final LockManager            lockManager;

    /**
     * 构造一个最大缓存 numPages 个页面的 BufferPool
     * @param numPages 缓存中最多可以容纳的页面数
     */
    public BufferPool(int numPages) {
        this.lruCache = new LruCache<>(numPages); // 初始化 LRU 缓存
        this.lockManager = new LockManager(); // 初始化锁管理器
    }

    // 静态方法注释

    /**
     * 获取当前页面大小（字节数）
     *
     * @return 页面大小
     */
    public static int getPageSize() {
        return pageSize;
    }

    /**
     * 设置页面大小（仅用于测试）
     *
     * @param pageSize 新的页面大小
     */
    public static void setPageSize(int pageSize) {
        BufferPool.pageSize = pageSize;
    }

    /**
     * 重置页面大小为默认值（仅用于测试）
     */
    public static void resetPageSize() {
        BufferPool.pageSize = DEFAULT_PAGE_SIZE;
    }

    // 核心方法注释

    /**
     * 获取指定页面并申请相应的访问权限（读/写）。
     * 会尝试获取锁，如果被其他事务持有则可能阻塞。
     *
     * 页面首先从缓存中查找：
     * 1、如果存在，直接返回；
     * 2、如果不存在，则从磁盘加载并加入缓存；
     * 3、若缓存已满，淘汰一个页面后插入新页面。
     *
     * @param tid 请求页面的事务 ID
     * @param pid 请求的页面 ID
     * @param perm 请求的访问权限（READ_ONLY / READ_WRITE）
     * @return 返回请求的页面对象
     * @throws TransactionAbortedException 如果事务等待锁超时或被中断
     * @throws DbException 如果读取页面失败
     */
    public Page getPage(TransactionId tid, PageId pid, Permissions perm) throws TransactionAbortedException, DbException {

        // 将权限转换为锁类型（0 表示共享锁 S，1 表示排他锁 X）
        final int lockType = perm == Permissions.READ_ONLY ? 0 : 1;

        // 模拟真实场景，随机生成一个锁等待超时时间（1000 - 2999ms）
        final int timeout = new Random().nextInt(2000) + 1000;

        // 尝试获取锁：参数 （页面 id，事务 id，锁类型，超时时间）
        if (!this.lockManager.tryAcquireLock(pid, tid, lockType, timeout)) {
            throw new TransactionAbortedException();
        }

        // 查找缓存
        final Page page = this.lruCache.get(pid);

        if (page != null) {
            // 命中缓存，直接返回
            return page;
        }
        // 未命中缓存，从磁盘加载并加入缓存
        return loadPageAndCache(pid);
    }

    /**
     * 从磁盘加载页面并缓存
     *
     * @param pid 页面 ID
     * @return 加载后的页面对象
     * @throws DbException 如果读取失败
     */
    private Page loadPageAndCache(final PageId pid) throws DbException {

        // 通过 Catalog 获取相应的数据库文件，如 HeapFile
        final DbFile dbFile = Database.getCatalog().getDatabaseFile(pid.getTableId());

        // 从磁盘读取页面
        final Page dbPage = dbFile.readPage(pid);

        if (dbPage != null) {
            // 将页面放入 LRU 缓存，若存在则进行更新

            this.lruCache.put(pid, dbPage);
            // 如果缓存已满，淘汰最近最少使用的页面
            if (this.lruCache.getSize() == this.lruCache.getMaxSize()) {
                evictPage();
            }
        }
        return dbPage; // 返回加载后的页面（可能为 null，需上层处理）
    }

    /**
     * 强制释放某个事务对页面的锁（不推荐调用）
     * 调用此方法风险较高，可能导致系统行为异常，请谨慎使用。
     *
     * @param tid 请求释放锁的事务 ID
     * @param pid 要释放锁的页面 ID
     */
    public void unsafeReleasePage(TransactionId tid, PageId pid) {
        this.lockManager.releaseLock(pid, tid); // 释放锁
    }

    /**
     * 释放事务持有的所有锁（通常在事务完成时调用）
     *
     * @param tid 请求释放锁的事务 ID
     */
    public void transactionComplete(TransactionId tid) {
        // lab1/lab2 不需要实现
    }

    /**
     * 判断事务是否持有该页面的锁
     *
     * @param tid 要检查的事务 ID
     * @param p   页面 ID
     * @return 是否持有锁
     */
    public boolean holdsLock(TransactionId tid, PageId p) {
        return this.lockManager.holdsLock(p, tid); // 查询锁状态
    }

    /**
     * 提交或回滚事务，并释放其持有的所有锁。
     *
     * @param tid 事务 ID
     * @param commit 是否提交（true）或回滚（false）
     */
    public void transactionComplete(TransactionId tid, boolean commit) {
        try {
            if (commit) {
                // 如果提交事务，刷脏页到磁盘
                flushPages(tid);
            } else {
                // 如果回滚事务，重新加载原始数据
                reLoadPages(tid);
            }
            this.lockManager.releaseLockByTxn(tid); // 释放事务的所有锁
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 代表事务 tid 向指定表添加一个元组。此操作会获取写锁（对于 lab2 来说不需要）。
     * 如果无法获取锁，可能会阻塞。
     *
     * 标记所有被该操作弄脏的页面，并将这些页面的版本加入缓存中（替换任何现有版本），以确保未来的请求看到最新的页面。
     *
     * @param tid 执行添加操作的事务 ID
     * @param tableId 要添加元组的表 ID
     * @param t 要添加的元组
     */
    public void insertTuple(TransactionId tid, int tableId, Tuple t) throws DbException, IOException, TransactionAbortedException {

        final DbFile table = Database.getCatalog().getDatabaseFile(tableId); // 获取对应表文件

        final List<Page> dirtyPages = table.insertTuple(tid, t); // 插入元组并获取更新的页面列表

        for (final Page page : dirtyPages) { // 遍历所有被修改的页面
            page.markDirty(true, tid); // 标记页面为脏页
            this.lruCache.put(page.getId(), page); // 更新缓存中的页面
        }
    }

    /**
     * 从缓冲池中移除指定元组。移除时会对涉及的页面加写锁（lab2 不需要）。
     * 如果无法获取锁，可能会阻塞。
     *
     * 标记所有被该操作弄脏的页面，并将这些页面的版本加入缓存中（替换任何现有版本），以确保未来的请求看到最新的页面。
     *
     * @param tid 删除元组的事务 ID
     * @param t 要删除的元组
     */
    public void deleteTuple(TransactionId tid, Tuple t) throws DbException, IOException, TransactionAbortedException {

        int tableId = t.getRecordId().getPageId().getTableId(); // 获取元组所属表ID

        DbFile table = Database.getCatalog().getDatabaseFile(tableId); // 获取对应表文件

        final List<Page> dirtyPages = table.deleteTuple(tid, t); // 删除元组并获取更新的页面列表

        for (final Page page : dirtyPages) { // 遍历所有被修改的页面
            page.markDirty(true, tid); // 标记页面为脏页
            this.lruCache.put(page.getId(), page); // 更新缓存中的页面
        }
    }

    /**
     * 将所有脏页刷新到磁盘。
     * 注意：谨慎使用此方法，因为它会将脏数据写入磁盘，如果在 NO STEAL 模式下运行会导致 simpledb 出错。
     */
    public synchronized void flushAllPages() throws IOException {

        final Iterator<Page> pageIterator = this.lruCache.valueIterator(); // 获取缓存中所有页面的迭代器

        while (pageIterator.hasNext()) {
            flushPage(pageIterator.next()); // 刷新每个页面
        }
    }

    /**
     * 从缓冲池中移除特定页面 ID。
     * 此方法由恢复管理器使用，以确保缓冲池不会保留已回滚的页面在其缓存中。
     * 也由 B+ 树文件使用，确保已删除的页面可以从缓存中安全地重用。
     */
    public synchronized void discardPage(PageId pid) {
        this.lruCache.remove(pid); // 从缓存中移除指定页面
    }

    /**
     * 刷新特定页面到磁盘。
     * @param page 需要刷新的页面
     */
    private synchronized void flushPage(Page page) {
        try {
            final DbFile tableFile = Database.getCatalog().getDatabaseFile(page.getId().getTableId()); // 获取表文件
            tableFile.writePage(page); // 写页面到磁盘
            page.markDirty(false, null); // 清除脏页标志
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Error happen when flush page to disk:" + e.getMessage());
        }
    }

    /**
     * 刷新指定事务修改的所有脏页
     */
    public synchronized void flushPages(TransactionId tid) throws IOException {
        final Iterator<Page> pageIterator = this.lruCache.valueIterator(); // 获取缓存中所有页面的迭代器
        while (pageIterator.hasNext()) {
            final Page page = pageIterator.next();
            if (page.isDirty() == tid) { // 如果页面是当前事务弄脏的
                flushPage(page); // 刷新页面
            }
        }
    }

    /**
     * 从磁盘重新加载指定事务的所有页面。
     */
    public synchronized void reLoadPages(TransactionId tid) throws IOException, DbException {
        final Iterator<Page> pageIterator = this.lruCache.valueIterator(); // 获取缓存中所有页面的迭代器
        while (pageIterator.hasNext()) {
            final Page page = pageIterator.next();
            if (page.isDirty() == tid) { // 如果页面是当前事务弄脏的
                discardPage(page.getId()); // 移除缓存中的页面
                loadPageAndCache(page.getId()); // 重新加载页面
            }
        }
    }

    /**
     * 淘汰最近最少使用的页面（LRU 策略）
     * 刷新页面到磁盘以确保脏页在磁盘上得到更新。
     */
    private synchronized void evictPage() throws DbException {
        final Iterator<Page> pageIterator = this.lruCache.reverseIterator(); // 获取反向迭代器（最近最少使用的页面）
        while (pageIterator.hasNext()) {
            final Page page = pageIterator.next();
            if (page.isDirty() == null) { // 如果页面不是脏页
                discardPage(page.getId()); // 移除页面
                return;
            }
        }
        throw new DbException("All pages are dirty in buffer pool"); // 如果所有页面都是脏页，则抛出异常
    }
}
